# 
#  Copyright (C) 2005 Christopher J. Stawarz <cstawarz@cfa.harvard.edu>
# 
#  This file is part of PySL.
# 
#  PySL is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
# 
#  PySL is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
# 
#  You should have received a copy of the GNU General Public License
#  along with PySL; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#


"""
Use S-Lang functions and variables from Python

PySL is a package that makes S-Lang functions and variables accessible via
Python.  It allows any S-Lang data type to move from S-Lang to Python scope
and back and lets the user call any intrinsic or user-defined S-Lang function
(including those defined by imported modules) from Python.  All of this can
be accomplished using a simple, module-like interface.

PySL uses numarray to provide the Python-side equivalent of S-Lang arrays.  In
addition, it defines two custom data types for representing S-Lang objects
without Python equivalents:  SLangStruct, for S-Lang structures, and
SLangObject, which encapsulates a pointer to a S-Lang object whose type has no
native representation in Python (such as BString_Type, File_Type, or types
defined by extension modules).

The various classes and functions provided by PySL have their own
documentation, but as a quick introduction, typing "from pysl import sl" at
the interactive prompt allows one to do the following:

>>> sl.eval('define add(a,b) { return a+b; }')
>>> sl.add(3,4)
7
>>> sl.add('foo','bar')
'foobar'
>>> sl.add([1,2,3], [4,5,6])
array([5, 7, 9])
>>> sl.max([12,-4,72,3])
72
>>> sl.eval(\"\"\"
... variable a = ["one","two","three","four","five","six"];
... reshape(a, [2,3]);
...
... variable b = [1.1, 2.2, 3.3, 4.4];
... reshape(b, [2,2]);
...
... variable c = struct { f1, f2 };
... c.f1 = a;
... c.f2 = b;
... \"\"\")
>>> sl.a
ObjectArray([['one', 'two', 'three'],
             ['four', 'five', 'six']])
>>> sl.b
array([[ 1.1,  2.2],
       [ 3.3,  4.4]])
>>> c = sl.c
>>> c
SLangStruct([('f1', ObjectArray([['one', 'two', 'three'],
             ['four', 'five', 'six']])), ('f2', array([[ 1.1,  2.2],
       [ 3.3,  4.4]]))], 'Struct_Type')
>>> print c
f1 = [['one' 'two' 'three']
      ['four' 'five' 'six']]
f2 = [[ 1.1  2.2]
      [ 3.3  4.4]]
>>> c.f2
array([[ 1.1,  2.2],
       [ 3.3,  4.4]])
>>> sl.a = 'Hello, world!'
>>> sl.a
'Hello, world!'
>>> sl.d = {'foo':1, 'bar':2}
>>> sl.eval('string(typeof(d))')
'Assoc_Type'
>>> sl.eval(\"\"\"
... implements("foo");
... variable foo1 = 3;
... variable foo2 = "Hello from foo namespace!";
... \"\"\")
>>> sl.foo1
Traceback (most recent call last):
...
NameError: S-Lang variable 'foo1' is not defined
>>> sl.foo.foo1
3
>>> sl.foo.foo2
'Hello from foo namespace!'
"""


__version__ = "0.1.0"

import numarray
import numarray.objects


class SLangStruct(object):
   """
   Python-side representation of a S-Lang structure

   A SLangStruct is the Python equivalent of a S-Lang structure.  It functions
   much like a dictionary, except that the keys have a fixed order, and new
   keys cannot be added to an existing instance.

   A SLangStruct is created automatically whenever a structure moves from
   S-Lang to Python scope:

   >>> load_string(\"\"\"
   ... variable ss_s = struct { a, b };
   ... ss_s.a = 1.2;
   ... ss_s.b = "foo";
   ... \"\"\")
   >>> ss_s = sl.ss_s
   >>> ss_s
   SLangStruct([('a', 1.2), ('b', 'foo')], 'Struct_Type')
   >>> print ss_s
   a = 1.2
   b = foo
   >>> ss_s.b
   'foo'
   """

   def __init__(self, item_list, type_name='Struct_Type'):
      """
      The SLangStruct constructor has one required argument, a list of two-item
      tuples containing field name/value pairs, and one optional argument, the
      name of a user-defined S-Lang data type created with typedef:

      >>> load_string('typedef struct { foo, bar } FooBar_Type;')
      >>> ss_cn_s1 = SLangStruct([('A',12),('B',3.4)])
      >>> ss_cn_s2 = SLangStruct([('foo','ham'),('bar','sam')], 'FooBar_Type')
      >>> print ss_cn_s1
      A = 12
      B = 3.4
      >>> print ss_cn_s2
      foo = ham
      bar = sam
      >>> sl.string(ss_cn_s1)
      'Struct_Type'
      >>> sl.string(ss_cn_s2)
      'FooBar_Type'
      """

      self.field_names = []

      for (field, value) in item_list:
	 self.field_names.append(field)
         self.__dict__[field] = value

      self.type_name = type_name

   def __repr__(self):
      return "%s(%s, '%s')" % (self.__class__.__name__, self.tolist(),
        self.type_name)

   def __str__(self):
      # Format the output so that one field is printed per line and the value
      # strings all start in the same column

      # Get length of longest field name
      field_width = max(map(len, self.field_names))

      # Create format string
      field_width_str = str(field_width)
      field_format = '%-' + field_width_str + '.' + field_width_str + 's = %s\n'

      # Replace embedded newlines in value strings with a padded newline so
      # that multiline values line up nicely
      padded_nl = '\n' + (field_width + 3) * ' ';

      s = ''
      for field in self.field_names:
	 value_str = str(self.__dict__[field]).replace('\n', padded_nl)
         s += field_format % (field, value_str)

      return s[:-1]  # Drop final newline

   def dup(self):
      """
      Create an identical copy of the SLangStruct:

      >>> ss_dp_s = SLangStruct([('a',1),('b',2.0)])
      >>> ss_dp_s
      SLangStruct([('a', 1), ('b', 2.0)], 'Struct_Type')
      >>> ss_dp_ss = ss_dp_s.dup()
      >>> ss_dp_ss
      SLangStruct([('a', 1), ('b', 2.0)], 'Struct_Type')
      >>> ss_dp_s is ss_dp_ss
      0
      """
      return SLangStruct(self.tolist(), self.type_name)

   def tolist(self):
      """
      Return a list of two-item tuples containing the SLangStruct's fields in
      name/value pairs:

      >>> ss_tl_s = SLangStruct([('a',1),('b',2.0)])
      >>> ss_tl_s.tolist()
      [('a', 1), ('b', 2.0)]
      """
      return zip(self.field_names, [self.__dict__[f] for f in self.field_names])

   def toslstring(self):
      """
      Return the S-Lang code needed to instantiate the data type the
      SLangStruct represents:

      >>> ss_ts_s = SLangStruct([('a',1),('b',2.0)])
      >>> ss_ts_s.toslstring()
      'struct { a, b }'
      >>> ss_ts_s = SLangStruct([('foo','ham'),('bar','sam')], 'FooBar_Type')
      >>> ss_ts_s.toslstring()
      '@FooBar_Type'
      """

      if (self.type_name == 'Struct_Type') or (not self.type_name):
         sl_str = 'struct { ' + ', '.join(self.field_names) + ' }'
      else:
         sl_str = '@' + self.type_name

      return sl_str


# Must do this after numarray.objects has been imported and SLangStruct has
# been defined
from _pysl import *


class SLangFunction(object):
   """
   Python interface to a S-Lang function

   A SLangFunction is a callable object that mediates the execution of a S-Lang
   function from Python scope.  One is created by passing the constructor the
   name of a S-Lang function and, optionally, a S-Lang namespace name.  It can
   then be called like any Python function:

   >>> sf_sl_string = SLangFunction('string')
   >>> sf_sl_string([1,2,3])
   'Integer_Type[3]'
   >>> load_string('define square(x) { return (x*x); }', 'sf_ns')
   >>> sf_square = SLangFunction('square')
   Traceback (most recent call last):
   ...
   NameError: S-Lang function 'square' is not defined
   >>> sf_square = SLangFunction('square', 'sf_ns')
   >>> sf_square(7)
   49
   """

   def __init__(self, func_name, ns_name=None):
      self.func = get_function(func_name, ns_name)

   def __call__(self, *args):
      return execute_function(self.func, args)


class SLangMarshaller(object):
   """
   Module-like interface to S-Lang functions and variables

   The SLangMarshaller class provides a convenient, module-like interface to
   S-Lang functions and variables.  By default, a SLangMarshaller will search
   for names in S-Lang's global namespace.  However, if the constructor is
   passed a namespace name, then only that namespace will be searched.  The
   object pysl.sl is a predefined SLangMarshaller for the S-Lang global
   namespace.

   Existing S-Lang functions and variables are accessed as attributes of the
   SLangMarshaller object:

   >>> load_string(\"\"\"
   ... variable sm_a = [1, 2, 3];
   ... define sm_square(x) { return (x*x); }
   ... \"\"\")
   >>> sm_m = SLangMarshaller()
   >>> sm_m.sm_a
   array([1, 2, 3])
   >>> sm_m.sm_square(3)
   9

   Similarly, to set the value of a S-Lang variable, assign to the appropriate
   attribute.  If the S-Lang variable does not exist, it will be declared
   automatically:

   >>> sl.sm_aa
   Traceback (most recent call last):
   ...
   NameError: S-Lang variable 'sm_aa' is not defined
   >>> sl.sm_aa = 'spam'
   >>> sl.sm_aa
   'spam'
   >>> sl.sm_aa = 3 + 4j
   >>> sl.sm_aa
   (3+4j)

   As noted above, a SLangMarshaller can be created so that it searches for
   S-Lang names only in a specified namespace:

   >>> load_string('variable sm_b = "In Global";')
   >>> load_string('variable sm_c = "In sm_ns";', 'sm_ns')
   >>> sm_ns = SLangMarshaller('sm_ns')
   >>> sl.sm_b
   'In Global'
   >>> sm_ns.sm_b
   Traceback (most recent call last):
   ...
   NameError: S-Lang variable 'sm_ns->sm_b' is not defined
   >>> sl.sm_c
   Traceback (most recent call last):
   ...
   NameError: S-Lang variable 'sm_c' is not defined
   >>> sm_ns.sm_c
   'In sm_ns'

   In addition, functions and variables in S-Lang namespaces can be accessed
   via the same syntax used for components of Python packages, providing a
   very natural-looking namespace hierarchy:

   >>> load_string('define sm_f() { return "In sm_ns"; }', 'sm_ns')
   >>> sl.sm_f()
   Traceback (most recent call last):
   ...
   NameError: S-Lang variable 'sm_f' is not defined
   >>> sl.sm_ns.sm_f()
   'In sm_ns'
   >>> sl.sm_ns.sm_g = 'also in sm_ns'
   >>> sl.eval('sm_ns->sm_g;')
   'also in sm_ns'
   """

   # FIXME: discuss limitations of module-like syntax wrt composite objects

   def __init__(self, ns_name=None):
      self.__dict__['_ns_name'] = ns_name

   def __setattr__(self, name, value):
      set_variable(name, value, self._ns_name)

   def __getattr__(self, name):
      if is_namespace(name):
	 # Namespace
         ns = SLangMarshaller(name)
         self.__dict__[name] = ns
	 return ns
      elif is_defined(name, self._ns_name) > 0:
	 # Function
         func = SLangFunction(name, self._ns_name)
         self.__dict__[name] = func
         return func
      else:
	 # Either a variable or undefined
         return get_variable(name, self._ns_name)


# Define the S-Lang function _pysl->clear_stack(), to be called in prompt()
# after each input line is executed
_sl_nsname = '_pysl'
load_string(r"""

% Clear stack by popping and printing its contents
define clear_stack() {
   while (_stkdepth())
      message(string());
}

""", _sl_nsname)
clear_stack = SLangFunction('clear_stack', _sl_nsname)
_sl_prompt = 'sl> '

def prompt():
   """
   Start interactive S-Lang input.  As each line is entered, it is evaluated,
   and the contents of the stack are printed.

   To exit, use the same key sequence used to exit the Python interpreter
   (Ctrl-D on Unix-like systems).
   """

   try:
      import readline
   except ImportError:
      # No readline; this is annoying for the user but non-fatal
      pass

   try:
      while True:
         input = raw_input(_sl_prompt)

	 try:
	    load_string('_auto_declare = 1; ' + input, 'Global')
	 except SLangError, err:
	    # Only the first line of the error message is relevant; the rest
	    # is trace info, which is meaningless in interactive input
	    err_lines = str(err).split('\n  ')
	    print err_lines[len(err_lines) > 1]

	 clear_stack()
   except EOFError:
      print  # Start new line


# Create the default SLangMarshaller and add the S-Lang import() function to
# it with the name "slimport"
sl = SLangMarshaller()
sl.__dict__['slimport'] = SLangFunction('import')


#
# doctest stuff
#

other_tests="""
>>> __version__
'0.1.0'
>>> set_variable('message', 3)
Traceback (most recent call last):
...
NameError: 'message' is a S-Lang function name
>>> ot_ms = get_function('message')
>>> try:
...    execute_function(ot_ms, (3,))
... except SLangError, err:
...    print str(err)[1:]
...
  S-Lang Error: Type Mismatch: Unable to typecast Integer_Type to String_Type
  S-Lang Error: Type Mismatch: Error while executing message
>>> load_string('define echo_obj(x) { string(typeof(x)); x; }')
>>> sl.echo_obj(None)
('Null_Type', None)
>>> sl.echo_obj(7)
('Integer_Type', 7)
>>> sl.echo_obj(100L)
('Integer_Type', 100)
>>> sl.echo_obj(100000000000000000000000000000000000000000000000000L)
Traceback (most recent call last):
...
OverflowError: long int too large to convert to int
>>> sl.echo_obj(8.0)
('Double_Type', 8.0)
>>> sl.echo_obj(8.0 + 9.0j)
('Complex_Type', (8+9j))
>>> sl.echo_obj('foo')
('String_Type', 'foo')
>>> sl.echo_obj(u'foo')
('String_Type', 'foo')
>>> si = get_variable('stdin')
>>> tstr, obj = sl.echo_obj(si)
>>> tstr
'File_Type'
>>> type(obj)
<type 'pysl.SLangObject'>
>>> print obj
File_Type
>>> sl.echo_obj((1,2,3))
('Array_Type', array([1, 2, 3]))
>>> sl.echo_obj(['foo','bar','blah'])
('Array_Type', ObjectArray(['foo', 'bar', 'blah']))
>>> from numarray import *
>>> load_string(\"\"\"
... define echo_arr(a) {
...    sprintf("%S[%S]", typeof(a), _typeof(a));
...    a;
... }\"\"\")
>>> sl.echo_arr(array([0,1,2], Bool))
('Array_Type[Char_Type]', array([0, 1, 1], type=Int8))
>>> sl.echo_arr(array([1,2,3], Int8))
('Array_Type[Char_Type]', array([1, 2, 3], type=Int8))
>>> sl.echo_arr(array([1,2,3], UInt8))
('Array_Type[UChar_Type]', array([1, 2, 3], type=UInt8))
>>> sl.echo_arr(array([1,2,3], Int16))
('Array_Type[Short_Type]', array([1, 2, 3], type=Int16))
>>> sl.echo_arr(array([1,2,3], UInt16))
('Array_Type[UShort_Type]', array([1, 2, 3], type=UInt16))
>>> sl.echo_arr(array([1,2,3], Int32))
('Array_Type[Integer_Type]', array([1, 2, 3]))
>>> sl.echo_arr(array([1,2,3], UInt32))
('Array_Type[UInteger_Type]', array([1, 2, 3], type=UInt32))
>>> sl.echo_arr(array([1,2,3], Int64))
Traceback (most recent call last):
...
TypeError: cannot convert Int64 NumArray to S-Lang array
>>> sl.echo_arr(array([1,2,3], UInt64))
Traceback (most recent call last):
...
TypeError: cannot convert UInt64 NumArray to S-Lang array
>>> sl.echo_arr(array([1,2,3], Float32))
('Array_Type[Float_Type]', array([ 1.,  2.,  3.], type=Float32))
>>> sl.echo_arr(array([1,2,3], Float64))
('Array_Type[Double_Type]', array([ 1.,  2.,  3.]))
>>> sl.echo_arr(array([1,2,3], Complex32))
('Array_Type[Complex_Type]', array([ 1.+0.j,  2.+0.j,  3.+0.j]))
>>> sl.echo_arr(array([1,2,3], Complex64))
('Array_Type[Complex_Type]', array([ 1.+0.j,  2.+0.j,  3.+0.j]))
>>> import numarray.objects as obj
>>> sl.echo_arr(obj.array([['spam', 'ham'], ['yam', 'jam']]))
('Array_Type[String_Type]', ObjectArray([['spam', 'ham'],
             ['yam', 'jam']]))
>>> tstr, obj = sl.echo_arr(obj.array([['spam', 'ham'], ['yam', 7.2]]))
>>> tstr
'Array_Type[Any_Type]'
>>> obj.shape
(2, 2)
>>> type(obj[1,0])
<type 'pysl.SLangObject'>
>>> print obj[1,0]
Any_Type
>>> ot_d = {'a':1, 'b':2}
>>> tstr, obj = sl.echo_obj(ot_d)
>>> tstr
'Assoc_Type'
>>> obj == ot_d
1
>>> obj is ot_d
0
>>> try:
...    sl.echo_obj({1:1.1, 2:2.2})
... except SLangError, err:
...    print str(err)[1:]
...
  S-Lang Error: Type Mismatch: Unable to typecast Integer_Type to String_Type
  S-Lang Error: Type Mismatch: Assoc_Type arrays require a single string index
  S-Lang Error: Type Mismatch: called from line 1, file: ***string***
  S-Lang Error: Type Mismatch: called from eval: ($0,$1,$2) = (); $0[$1] = $2;
>>> sl.echo_obj(SLangStruct([('A', 1), ('B', 2)]))
('Struct_Type', SLangStruct([('A', 1), ('B', 2)], 'Struct_Type'))
>>> load_string('typedef struct { a, b, c } ABC_Type;')
>>> sl.echo_obj(SLangStruct([('a', 1), ('b', 2), ('c', 3)], 'ABC_Type'))
('ABC_Type', SLangStruct([('a', 1), ('b', 2), ('c', 3)], 'ABC_Type'))
>>> sl.echo_obj(eval)
Traceback (most recent call last):
...
TypeError: cannot convert builtin_function_or_method to S-Lang object
>>> sl.echo_obj([1,2.3,eval])
Traceback (most recent call last):
...
TypeError: cannot convert builtin_function_or_method to S-Lang object
>>> None is sl.eval('NULL')
1
>>> sl.eval('typecast(7, Char_Type)')
7
>>> sl.eval('typecast(7, UChar_Type)')
7
>>> sl.eval('typecast(7, Short_Type)')
7
>>> sl.eval('typecast(7, UShort_Type)')
7
>>> sl.eval('typecast(7, Int_Type)')
7
>>> sl.eval('typecast(7, UInt_Type)')
7L
>>> sl.eval('typecast(7, Long_Type)')
7
>>> sl.eval('typecast(7, ULong_Type)')
7L
>>> sl.eval('typecast(7, Float_Type)')
7.0
>>> sl.eval('typecast(7, Double_Type)')
7.0
>>> sl.eval('1+2i')
(1+2j)
>>> sl.eval('"foo"')
'foo'
>>> sl.eval('typecast([1,2,3], Char_Type)')
array([1, 2, 3], type=Int8)
>>> sl.eval('typecast([1,2,3], UChar_Type)')
array([1, 2, 3], type=UInt8)
>>> sl.eval('typecast([1,2,3], Short_Type)')
array([1, 2, 3], type=Int16)
>>> sl.eval('typecast([1,2,3], UShort_Type)')
array([1, 2, 3], type=UInt16)
>>> sl.eval('typecast([1,2,3], Int_Type)')
array([1, 2, 3])
>>> sl.eval('typecast([1,2,3], UInt_Type)')
array([1, 2, 3], type=UInt32)
>>> sl.eval('typecast([1,2,3], Long_Type)')
array([1, 2, 3])
>>> sl.eval('typecast([1,2,3], ULong_Type)')
array([1, 2, 3], type=UInt32)
>>> sl.eval('typecast([1,2,3], Float_Type)')
array([ 1.,  2.,  3.], type=Float32)
>>> sl.eval('typecast([1,2,3], Double_Type)')
array([ 1.,  2.,  3.])
>>> sl.eval('typecast([1,2,3], Complex_Type)')
array([ 1.+0.j,  2.+0.j,  3.+0.j])
>>> sl.eval('["foo", "bar", "blah"]')
ObjectArray(['foo', 'bar', 'blah'])
>>> sl.eval(\"\"\"
... variable ot_A = Assoc_Type[Integer_Type];
... ot_A["alpha"] = 1;
... ot_A["beta"] = 2;
... ot_A["gamma"] = 3;
... \"\"\")
>>> sl.ot_A == {'alpha': 1, 'beta': 2, 'gamma': 3}
1
>>> sl.eval(\"\"\"
... variable ot_s = struct { a, b };
... ot_s.a = 1;
... ot_s.b = "foobar";
... \"\"\")
>>> sl.ot_s
SLangStruct([('a', 1), ('b', 'foobar')], 'Struct_Type')
>>> sl.eval(\"\"\"
... variable ot_s2 = @ABC_Type;
... ot_s2.a = "ham";
... ot_s2.b = 12.0;
... ot_s2.c = [1, 2, 3];
... \"\"\")
>>> sl.ot_s2
SLangStruct([('a', 'ham'), ('b', 12.0), ('c', array([1, 2, 3]))], 'ABC_Type')
>>> ot_si = sl.eval('stdin')
>>> type(ot_si)
<type 'pysl.SLangObject'>
>>> print ot_si
File_Type
"""

__test__ = {
   'load_string':	load_string.__doc__,
   'load_file':		load_file.__doc__,
   'is_defined':	is_defined.__doc__,
   'is_namespace':	is_namespace.__doc__,
   'get_variable':	get_variable.__doc__,
   'set_variable':	set_variable.__doc__,
   'get_function':	get_function.__doc__,
   'execute_function':	execute_function.__doc__,
   'other_tests':	other_tests
}

def _test():
   import doctest, sys
   return doctest.testmod(sys.modules["__main__"])

if __name__ == "__main__":
   _test()

